---
id: URLencode
title: 谈谈前端 URL 编码问题
displayed_sidebar: baseSidebar
---

import Thumbnail from "@site/src/components/Thumbnail";
import Details from "@theme/Details";

## 前言

前端总是会听到，这样一句话 “你把，URL 编码一下就可以了” 😁，写这篇文章是我在使用项目[`encodeURIComponent`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) 方法的时候，突然想搞明白为何会有这东西，本着程序猿喜欢深挖技术源头的作风，决定研究一下

:::tip
一般来说，URL 只能使用英文字母、阿拉伯数字和某些标点符号，不能使用其他文字和符号。比如，世界上有英文字母的网址"http://www.abc.com"，但是没有希腊字母的网址"http://www.aβγ.com"（读作阿尔法-贝塔-伽玛.com）。这是因为网络标准RFC 1738
做了硬性规定
:::

## 网络标准 RFC 1738

<Details summary={<summary>网络标准RFC 1738规定</summary>} open>
  <p>
    "...Only alphanumerics [0-9a-zA-Z], the special characters "$-_.+!*'(),"
    [not including the quotes - ed], and reserved characters used for their
    reserved purposes may be used unencoded within a URL.”
  </p>
  <p>
    只有字母和数字[0-9a-zA-Z]、一些特殊符号"$-\_.+!\*'(),"[不包括双引号]、以及某些保留字，才可以不经过编码直接用于
    URL。
  </p>
</Details>

这意味着，如果`URL`中有汉字，就必须编码后使用。但是麻烦的是，RFC 1738 没有规定具体的编码方法，而是交给应用程序（浏览器）自己决定。这导致**URL 编码成为了一个混乱的领域**。

下面就让我们看看，"URL 编码"到底有多混乱。我会依次分析四种不同的情况，在每一种情况中，浏览器的 URL 编码方法都不一样。把它们的差异解释清楚之后，我再说如何用 Javascript 找到一个统一的编码方法。

## 情况 1：网址路径中包含汉字

打开 IE（我用的是 8.0 版），输入网址<span>http://zh.wikipedia.org/wiki/春节</span>。注意，"春节"这两个字此时是网址路径的一部分。

<Thumbnail
  src="/myimage/url1.png"
  alt="Choose either AWS or GCP"
  width="556px"
/>

### 结论

查看 HTTP 请求的头信息，会发现 IE 实际查询的网址是http://zh.wikipedia.org/wiki/%E6%98%A5%E8%8A%82
。也就是说，IE 自动将"春节"编码成了<code>%E6%98%A5%E8%8A%82</code>。

<Thumbnail
  src="/myimage/url2.png"
  alt="Choose either AWS or GCP"
  width="556px"
/>
我们知道，"春"和"节"的 utf-8 编码分别是"E6 98 A5"和"E8 8A 82"，因此，"%E6%98%A5%E8%8A%82"就是按照顺序，在每个字节前加上%而得到的。
在 Firefox 中测试，也得到了同样的结果

**结论 1 就是，网址路径的编码，用的是 utf-8 编码。**

## 情况 2：查询字符串包含汉字

在 IE 中输入网址<span>http://www.baidu.com/s?wd=春节</span>。注意，"春节"这两个字此时属于查询字符串，不属于网址路径，不要与情况 1 混淆:

<Thumbnail
  src="/myimage/url3.png"
  alt="Choose either AWS or GCP"
  width="556px"
/>

查看 HTTP 请求的头信息，会发现 IE 将"春节"转化成了一个乱码:

<Thumbnail
  src="/myimage/url4.png"
  alt="Choose either AWS or GCP"
  width="556px"
/>

切换到十六进制方式，才能清楚地看到，"春节"被转成了"B4 BA BD DA":

<Thumbnail
  src="/myimage/url5.png"
  alt="Choose either AWS or GCP"
  width="556px"
/>

我们知道，"春"和"节"的 GB2312 编码（我的操作系统"Windows XP"中文版的默认编码）分别是"B4 BA"和"BD DA"。因此，IE 实际上就是将查询字符串，以 GB2312 编码的格式发送出去。

Firefox 的处理方法，略有不同。它发送的 HTTP Head 是"wd=%B4%BA%BD%DA"。也就是说，同样采用 GB2312 编码，但是在每个字节前加上了%。

<Thumbnail
  src="/myimage/url6.png"
  alt="Choose either AWS or GCP"
  width="556px"
/>

### 结论

**查询字符串的编码，用的是操作系统的默认编码**

## 情况 3：Get 方法生成的 URL 包含汉字

前面说的是直接输入网址的情况，但是更常见的情况是，在已打开的网页上，直接用 Get 或 Post 方法发出 HTTP 请求。

根据台湾中兴大学[吕瑞麟老师的试验](http://web.nchu.edu.tw/~jlu/classes/xml/ajax/urlencoding.shtml)，这时的编码方法由网页的编码决定，也就是由 HTML 源码中字符集的设定决定。

```html
<meta http-equiv="Content-Type" content="text/html;charset=xxxx" />
```

如果上面这一行最后的 charset 是 UTF-8，则 URL 就以 UTF-8 编码；如果是 GB2312，URL 就以 GB2312 编码。

举例来说，百度是 GB2312 编码，Google 是 UTF-8 编码。因此，从它们的搜索框中搜索同一个词"春节"，生成的查询字符串是不一样的。

百度生成的是`%B4%BA%BD%DA`，这是 GB2312 编码:

<Thumbnail
  src="/myimage/url7.png"
  alt="Choose either AWS or GCP"
  width="556px"
/>

Google 生成的是%E6%98%A5%E8%8A%82，这是 UTF-8 编码:

<Thumbnail
  src="/myimage/url8.png"
  alt="Choose either AWS or GCP"
  width="556px"
/>

### 结论

**GET 和 POST 方法的编码，用的是网页的编码**

## 情况 4：Ajax 调用的 URL 包含汉字

前面三种情况都是由浏览器发出 HTTP 请求，最后一种情况则是由 Javascript 生成 HTTP 请求，也就是 Ajax 调用。还是根据吕瑞麟老师的文章，在这种情况下，IE 和 Firefox 的处理方式完全不一样。

举例来说，有这样两行代码：

```js
// 假定用户在表单中提交的值是"春节"这两个字
url = url + "?q=" + document.myform.elements[0].value;

http_request.open("GET", url, true);
```

### 结论

那么，无论网页使用什么字符集，IE 传送给服务器的总是"q=%B4%BA%BD%DA"，而 Firefox 传送给服务器的总是"q=%E6%98%A5%E8%8A%82"。也就是说，**在 Ajax 调用中，IE 总是采用 GB2312 编码（操作系统的默认编码），而 Firefox 总是采用 utf-8 编码。这就是我们的结论 4**

## [escape()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/escape)

好了，到此为止，四种情况都说完了。

假定前面你都看懂了，那么此时你应该会感到很头痛。因为，实在太混乱了。不同的操作系统、不同的浏览器、不同的网页字符集，将导致完全不同的编码结果。如果程序员要把每一种结果都考虑进去，是不是太恐怖了？有没有办法，能够保证客户端只用一种编码方法向服务器发出请求？

回答是有的，就是使用 Javascript 先对 URL 编码，然后再向服务器提交，不要给浏览器插手的机会。因为 Javascript 的输出总是一致的，所以就保证了服务器得到的数据是格式统一的。

Javascript 语言用于编码的函数，一共有三个，最古老的一个就是 escape()。虽然这个函数现在已经不提倡使用了，但是由于历史原因，很多地方还在使用它，所以有必要先从它讲起。

实际上，escape()不能直接用于 URL 编码，它的真正作用是返回一个字符的 Unicode 编码值。比如"春节"的返回结果是%u6625%u8282，也就是说在 Unicode 字符集中，"春"是第 6625 个（十六进制）字符，"节"是第 8282 个（十六进制）字符。

### 演示

```jsx live noInline
const Escape = () => {
  return (
    <ul>
      <li>
        <span>escape("春节"):</span>
        <br />
        输出：{escape("春节")}
      </li>
      <li>
        <span>escape("Hello World"):</span>
        <br />
        输出：{escape("Hello World")}
      </li>
      <li>
        <span>escape("\u6625\u8282"):</span>
        <br />
        输出：{escape("\u6625\u8282")}
      </li>
      <li>
        <span>unescape("%u6625%u8282"):</span>
        <br />
        输出：{unescape("%u6625%u8282")}
      </li>
      <li>
        <span>unescape("\u6625\u8282"):</span>
        <br />
        输出：{unescape("\u6625\u8282")}
      </li>
    </ul>
  );
};

render(
  <>
    <Escape />
  </>
);
```

### 规则

它的具体规则是，除了**ASCII 字母、数字、标点符号"@ \* \_ + - . /"**以外，对其他所有字符进行编码。在**\u0000 到\u00ff**之间的符号被转成**%xx**的形式，其余符号被转成**%uxxxx**的形式。对应的解码函数是`unescape()`。

所以，<u>Hello World</u>的`escape()`编码就是<u>Hello%20World</u>。因为空格的 Unicode 值是 20（十六进制）。
还有两个地方需要注意。

首先，无论网页的原始编码是什么，一旦被 Javascript 编码，就都变为 unicode 字符。也就是说，Javascipt 函数的输入和输出，默认都是 Unicode 字符。这一点对下面两个函数也适用。

其次，`escape()`不对**"+"**编码。但是我们知道，网页在提交表单的时候，如果有空格，则会被转化为**"+"**字符。服务器处理数据的时候，会把**"+"**号处理成空格。所以，使用的时候要小心。

## [encodeURI()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/encodeURI)

`encodeURI()`是 Javascript 中真正用来对 URL 编码的函数。

### 演示

```jsx live noInline
const Escape = () => {
  return (
    <ul>
      <li>
        <span>
          encodeURI("https://www.baidu.com?arg1=jx&arg2=Hello World"):
        </span>
        <br />
        输出：{encodeURI("https://www.baidu.com?arg1=jx&arg2=Hello World")}
      </li>
      <li>
        <span>encodeURI("春节"):</span>
        <br />
        输出：{encodeURI("春节")}
      </li>
    </ul>
  );
};

render(
  <>
    <Escape />
  </>
);
```

### 规则

它着眼于对整个 URL 进行编码，因此除了常见的符号以外，对其他一些在网址中有特殊含义的符号**; / ? : @ & = + $ , #**，也不进行编码。编码后，它输出符号的 utf-8 形式，并且在每个字节前加上**%**

需要注意的是，它不对单引号**'**编码

## [encodeURIComponent()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent)

最后一个 Javascript 编码函数是`encodeURIComponent()`。与`encodeURI()`的区别是，它用于对 URL 的组成部分进行**个别编码，而不用于对整个 URL 进行编码**。

### 演示

```jsx live noInline
const Escape = () => {
  return (
    <ul>
      <li>
        <span>encodeURIComponent("mail@example.com"):</span>
        <br />
        输出：{encodeURIComponent("mail@example.com")}
      </li>
      <li>
        <span>encodeURI("mail@example.com"):</span>
        <br />
        输出：{encodeURI("mail@example.com")}
      </li>
    </ul>
  );
};

render(
  <>
    <Escape />
  </>
);
```

### 规则

因此，**; / ? : @ & = + $ , #**，这些在`encodeURI()`中不被编码的符号，在`encodeURIComponent()`中统统会被编码。至于具体的编码方法，两者是一样。

它对应的解码函数是`decodeURIComponent()`。

